import socket
from threading import Thread

class P2PClient(object):
    """
    client class, which provides the core functionality
    to connect to another node's server, enabling you
    to send data
    """
    
    #def __init__(self, onreceive, onconnect, onclose, ipv6: bool=False, buffersize: bool=1024):
    def __init__(self, onreceive, onconnect, onclose, ipv6=False, buffersize=1024):
        """
        initialize the class members as necessary
        """
        
        self.clientport = None
        
        self.hosts = {}
        self.onreceive = onreceive
        self.onconnect = onconnect
        self.onclose = onclose
        self.buffersize = buffersize
        
        self.ipv6 = ipv6
    
    #def __host_thread(self, hostsocket: socket, hostaddress: str):
    def __host_thread(self, hostsocket, hostaddress):
        # set the running state True
        hostrunning = True
        # start mainloop
        while hostrunning:
            # blocking receive method
            data = self.clientsocket.recv(self.buffersize)
            # trigger targeted method with the data
            self.onreceive(hostaddress, data)
            # if no data has arrived the socket is broken
            if data == '':
                hostrunning = False
                # trigger closed connection target
                self.onclose(hostaddress);
                self.__remove_host_from_dict(hostsocket)
    
    #def __get_hostsocket_by_address(self, address: str):
    def __get_hostsocket_by_address(self, address):
        hostsocket = None
        
        for key in self.clients:
            if self.clients[key]["address"] == address:
                hostsocket = key
        
        return hostsocket
    
    #def __remove_host_from_dict(self, hostsocket: socket):
    def __remove_host_from_dict(self, hostsocket):
        # client is disconnected, so remove it from the clients dict
        del self.hosts[hostsocket] # this is semantically better
    
    
    #def contains(self, address: str):
    def contains(self, address):
        if self.__get_hostsocket_by_address(address) == None:
            return False
        else:
            return True
    
    #def start(self, port: int):
    def start(self, port):
        """
        start the client
        """
        
        self.serverport = port
    
    def stop(self):
        """
        stop the client, all hosts will be
        disconnected
        """
        
        # kill all client threads
        for key in self.hosts:
            key.close()
            self.hosts[key]["thread"].join(0.1)
            self.onclose(address=self.hosts[key]["address"])
            self.__remove_client_from_dict(key)
        
        self.hosts.clear()
    
    #def connect(self, address: str):
    def connect(self, address):
        """
        connect to a another node participating
        in the network
        """
        
        # create a new socket
        if self.ipv6:
            hostsocket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
        else:
            hostsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # connect to a server
        hostsocket.connect((address, self.clientport))
        # create the client thread 
        hostthread = Thread(target=self.__host_thread)
        hostthread.daemon = True
        # add it to the hosts dict
        self.hosts[hostsocket] = {}
        self.hosts[hostsocket]["thread"] = hostthread
        self.hosts[hostsocket]["address"] = address
        # trigger new connection target
        self.onconnect(address, self.send);
        self.clientthread.start()
    
    #def close(self, address: str):
    def close(self, address):
        """
        disconnect from the node you are
        connected to
        """
        
        # get the corresponding host socket
        hostsocket = self.__get_hostsocket_by_address(address)
        # wait 0.1 seconds until killing the thread
        self.hosts[hostsocket]["thread"].join(0.1)
        # close the client socket
        hostsocket.close()
        # remove the hsot from the dict
        self.__remove_hosts_from_dict(hostsocket)
        self.onclose(address);
        # set the running state False
        self.clientrunning = False
    
    #def send(self, address: str, data: str):
    def send(self, address, data):
        """
        send data to the connected node
        """
        
        hostsocket = self.__get_clientsocket_by_address(address=address)
        # running state for the client socket
        hostrunning = True
        # count the data already sent
        totalsent = 0
        # loop until all data has been transmitted
        while hostrunning and totalsent < len(data):
            # fire the data
            sent = hostsocket.send(data[totalsent:])
            # if nothing has been sent the socket is broken
            if sent == 0:
                self.onclose(address)
                self.__remove_hosts_from_dict(hostsocket)
                hostrunning = False

